/*
* Copyright 2013 Georg Gruetter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jenkinsci.plugins.stashNotifier;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.*;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import hudson.Extension;
import hudson.Launcher;
import hudson.ProxyConfiguration;
import hudson.model.*;
import hudson.plugins.git.Revision;
import hudson.plugins.git.util.BuildData;
import hudson.security.ACL;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import jenkins.model.JenkinsLocationConfiguration;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException;
import org.jenkinsci.plugins.tokenmacro.TokenMacro;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
/**
* Notifies a configured Atlassian Stash server instance of build results
* through the Stash build API.
* <p>
* Only basic authentication is supported at the moment.
*/
public class StashNotifier extends Notifier {
public static final int MAX_FIELD_LENGTH = 255;
public static final int MAX_URL_FIELD_LENGTH = 450;
// attributes --------------------------------------------------------------
/** base url of Stash server, e. g. <tt>http://localhost:7990</tt>. */
private final String stashServerBaseUrl;
/** The id of the credentials to use. */
private String credentialsId;
/** if true, ignore exception thrown in case of an unverified SSL peer. */
private final boolean ignoreUnverifiedSSLPeer;
/** specify the commit from config */
private final String commitSha1;
/** if true, the build number is included in the Stash notification. */
private final boolean includeBuildNumberInKey;
/** specify project key manually */
private final String projectKey;
/** append parent project key to key formation */
private final boolean prependParentProjectKey;
/** whether to send INPROGRESS notification at the build start */
private final boolean disableInprogressNotification;
private JenkinsLocationConfiguration globalConfig = new JenkinsLocationConfiguration();
// public members ----------------------------------------------------------
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
@DataBoundConstructor
public StashNotifier(
String stashServerBaseUrl,
String credentialsId,
boolean ignoreUnverifiedSSLPeer,
String commitSha1,
boolean includeBuildNumberInKey,
String projectKey,
boolean prependParentProjectKey,
boolean disableInprogressNotification
) {
this.stashServerBaseUrl = stashServerBaseUrl.endsWith("/")
? stashServerBaseUrl.substring(0, stashServerBaseUrl.length() - 1)
: stashServerBaseUrl;
this.credentialsId = credentialsId;
this.ignoreUnverifiedSSLPeer
= ignoreUnverifiedSSLPeer;
this.commitSha1 = commitSha1;
this.includeBuildNumberInKey = includeBuildNumberInKey;
this.projectKey = projectKey;
this.prependParentProjectKey = prependParentProjectKey;
this.disableInprogressNotification = disableInprogressNotification;
}
public boolean isDisableInprogressNotification() {
return disableInprogressNotification;
}
public String getCredentialsId() {
return credentialsId;
}
public String getStashServerBaseUrl() {
return stashServerBaseUrl;
}
public boolean getIgnoreUnverifiedSSLPeer() {
return ignoreUnverifiedSSLPeer;
}
public String getCommitSha1() {
return commitSha1;
}
public boolean getIncludeBuildNumberInKey() {
return includeBuildNumberInKey;
}
public String getProjectKey() {
return projectKey;
}
public boolean getPrependParentProjectKey() {
return prependParentProjectKey;
}
@Override
public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) {
return disableInprogressNotification || processJenkinsEvent(build, listener, StashBuildState.INPROGRESS);
}
@Override
public boolean perform(
AbstractBuild<?, ?> build,
Launcher launcher,
BuildListener listener) {
if ((build.getResult() == null)
|| (!build.getResult().equals(Result.SUCCESS))) {
return processJenkinsEvent(
build, listener, StashBuildState.FAILED);
} else {
return processJenkinsEvent(
build, listener, StashBuildState.SUCCESSFUL);
}
}
/**
* Provide a fallback for getting the instance's root URL
*
* @return Root URL contained in the global config
*/
private String getRootUrl() {
return (Jenkins.getInstance().getRootUrl() != null) ? Jenkins.getInstance().getRootUrl() : globalConfig.getUrl();
}
/**
* Processes the Jenkins events triggered before and after the build and
* initiates the Stash notification.
*
* @param build the build to notify Stash of
* @param listener the Jenkins build listener
* @param state the state of the build (in progress, success, failed)
* @return always true in order not to abort the Job in case of
* notification failures
*/
private boolean processJenkinsEvent(
final AbstractBuild<?, ?> build,
final BuildListener listener,
final StashBuildState state) {
PrintStream logger = listener.getLogger();
// exit if Jenkins root URL is not configured. Stash build API
// requires valid link to build in CI system.
if (getRootUrl() == null) {
logger.println(
"Cannot notify Stash! (Jenkins Root URL not configured)");
return true;
}
Collection<String> commitSha1s = lookupCommitSha1s(build, listener);
for (String commitSha1 : commitSha1s) {
try {
NotificationResult result
= notifyStash(logger, build, commitSha1, listener, state);
if (result.indicatesSuccess) {
logger.println(
"Notified Stash for commit with id "
+ commitSha1);
} else {
logger.println(
"Failed to notify Stash for commit "
+ commitSha1
+ " (" + result.message + ")");
}
} catch (SSLPeerUnverifiedException e) {
logger.println("SSLPeerUnverifiedException caught while "
+ "notifying Stash. Make sure your SSL certificate on "
+ "your Stash server is valid or check the "
+ " 'Ignore unverifiable SSL certificate' checkbox in the "
+ "Stash plugin configuration of this job.");
} catch (Exception e) {
logger.println("Caught exception while notifying Stash with id "
+ commitSha1);
e.printStackTrace(logger);
}
}
if (commitSha1s.isEmpty()) {
logger.println("found no commit info");
}
return true;
}
private Collection<String> lookupCommitSha1s(
@SuppressWarnings("rawtypes") AbstractBuild build,
BuildListener listener) {
if (commitSha1 != null && commitSha1.trim().length() > 0) {
PrintStream logger = listener.getLogger();
try {
return Arrays.asList(TokenMacro.expandAll(build, listener, commitSha1));
} catch (IOException e) {
logger.println("Unable to expand commit SHA value");
e.printStackTrace(logger);
return Arrays.asList();
} catch (InterruptedException e) {
logger.println("Unable to expand commit SHA value");
e.printStackTrace(logger);
return Arrays.asList();
} catch (MacroEvaluationException e) {
logger.println("Unable to expand commit SHA value");
e.printStackTrace(logger);
return Arrays.asList();
}
}
// Use a set to remove duplicates
Collection<String> sha1s = new HashSet<String>();
// MultiSCM may add multiple BuildData actions for each SCM, but we are covered in any case
for (BuildData buildData : build.getActions(BuildData.class)) {
// get the sha1 of the commit that was built
Revision lastBuiltRevision = buildData.getLastBuiltRevision();
if (lastBuiltRevision == null) {
continue;
}
String lastBuiltSha1 = lastBuiltRevision.getSha1String();
// Should never be null, but may be blank
if (!lastBuiltSha1.isEmpty()) {
sha1s.add(lastBuiltSha1);
}
// This might be different than the lastBuiltSha1 if using "Merge before build"
String markedSha1 = buildData.lastBuild.getMarked().getSha1String();
// Should never be null, but may be blank
if (!markedSha1.isEmpty()) {
sha1s.add(markedSha1);
}
}
return sha1s;
}
/**
* Returns the HttpClient through which the REST call is made. Uses an
* unsafe TrustStrategy in case the user specified a HTTPS URL and
* set the ignoreUnverifiedSSLPeer flag.
*
* @param logger the logger to log messages to
* @param build
* @return the HttpClient
*/
protected HttpClient getHttpClient(PrintStream logger, AbstractBuild<?, ?> build) throws Exception {
boolean ignoreUnverifiedSSL = ignoreUnverifiedSSLPeer;
String stashServer = stashServerBaseUrl;
DescriptorImpl descriptor = getDescriptor();
CertificateCredentials certificateCredentials = getCredentials(CertificateCredentials.class, build.getProject());
if ("".equals(stashServer) || stashServer == null) {
stashServer = descriptor.getStashRootUrl();
}
if (!ignoreUnverifiedSSL) {
ignoreUnverifiedSSL = descriptor.isIgnoreUnverifiedSsl();
}
URL url = new URL(stashServer);
HttpClientBuilder builder = HttpClientBuilder.create();
if (url.getProtocol().equals("https")
&& (ignoreUnverifiedSSL || certificateCredentials instanceof CertificateCredentials)) {
// add unsafe trust manager to avoid thrown
// SSLPeerUnverifiedException
try {
SSLConnectionSocketFactory sslConnSocketFactory
= new SSLConnectionSocketFactory(buildSslContext(ignoreUnverifiedSSL,certificateCredentials),
ignoreUnverifiedSSL ? SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER : null);
builder.setSSLSocketFactory(sslConnSocketFactory);
Registry<ConnectionSocketFactory> registry
= RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslConnSocketFactory)
.build();
HttpClientConnectionManager ccm
= new BasicHttpClientConnectionManager(registry);
builder.setConnectionManager(ccm);
} catch (NoSuchAlgorithmException nsae) {
logger.println("Couldn't establish SSL context:");
nsae.printStackTrace(logger);
} catch (KeyManagementException kme) {
logger.println("Couldn't initialize SSL context:");
kme.printStackTrace(logger);
} catch (KeyStoreException kse) {
logger.println("Couldn't initialize SSL context:");
kse.printStackTrace(logger);
}
}
// Configure the proxy, if needed
// Using the Jenkins methods handles the noProxyHost settings
ProxyConfiguration proxyConfig = Jenkins.getInstance().proxy;
if (proxyConfig != null) {
Proxy proxy = proxyConfig.createProxy(url.getHost());
if (proxy != null && proxy.type() == Proxy.Type.HTTP) {
SocketAddress addr = proxy.address();
if (addr != null && addr instanceof InetSocketAddress) {
InetSocketAddress proxyAddr = (InetSocketAddress) addr;
HttpHost proxyHost = new HttpHost(proxyAddr.getAddress().getHostAddress(), proxyAddr.getPort());
builder = builder.setProxy(proxyHost);
String proxyUser = proxyConfig.getUserName();
if (proxyUser != null) {
String proxyPass = proxyConfig.getPassword();
BasicCredentialsProvider cred = new BasicCredentialsProvider();
cred.setCredentials(new AuthScope(proxyHost),
new org.apache.http.auth.UsernamePasswordCredentials(proxyUser, proxyPass));
builder = builder
.setDefaultCredentialsProvider(cred)
.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
}
}
}
}
return builder.build();
}
/**
* Helper in place to allow us to define out HttpClient SSL context
*
* @param ignoreUnverifiedSSL
* @param credentials
* @return
* @throws UnrecoverableKeyException
* @throws NoSuchAlgorithmException
* @throws KeyStoreException
* @throws KeyManagementException
*/
private SSLContext buildSslContext(boolean ignoreUnverifiedSSL, Credentials credentials) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
SSLContextBuilder customContext = SSLContexts.custom();
if (credentials instanceof CertificateCredentials) {
customContext = customContext.loadKeyMaterial(((CertificateCredentials) credentials).getKeyStore(),((CertificateCredentials) credentials).getPassword().getPlainText().toCharArray());
}
if (ignoreUnverifiedSSL) {
TrustStrategy easyStrategy = new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
return true;
}
};
customContext = customContext
.loadTrustMaterial(null, easyStrategy);
}
return customContext.useTLS().build();
}
@Override
public DescriptorImpl getDescriptor() {
// see Descriptor javadoc for more about what a descriptor is.
return (DescriptorImpl)super.getDescriptor();
}
@Extension
public static final class DescriptorImpl
extends BuildStepDescriptor<Publisher> {
/**
* To persist global configuration information,
* simply store it in a field and call save().
*
* <p>
* If you don't want fields to be persisted, use <tt>transient</tt>.
*/
private String credentialsId;
private String stashRootUrl;
private boolean ignoreUnverifiedSsl;
private boolean includeBuildNumberInKey;
private String projectKey;
private boolean prependParentProjectKey;
private boolean disableInprogressNotification;
public DescriptorImpl() {
load();
}
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item project) {
if (project == null || !project.hasPermission(Item.CONFIGURE)) {
return new StandardListBoxModel();
}
return new StandardListBoxModel()
.withEmptySelection()
.withMatching(
new StashCredentialMatcher(),
CredentialsProvider.lookupCredentials(
StandardCredentials.class,
project,
ACL.SYSTEM,
new ArrayList<DomainRequirement>()));
}
public String getStashRootUrl() {
if ((stashRootUrl == null) || (stashRootUrl.trim().equals(""))) {
return null;
} else {
return stashRootUrl;
}
}
public boolean isDisableInprogressNotification() {
return disableInprogressNotification;
}
public String getCredentialsId() {
return credentialsId;
}
public boolean isIgnoreUnverifiedSsl() {
return ignoreUnverifiedSsl;
}
public boolean isIncludeBuildNumberInKey() {
return includeBuildNumberInKey;
}
public String getProjectKey() {
return projectKey;
}
public boolean isPrependParentProjectKey() {
return prependParentProjectKey;
}
public FormValidation doCheckCredentialsId(@QueryParameter String value)
throws IOException, ServletException {
if (value.trim().equals("")) {
return FormValidation.error(
"Please specify the credentials to use");
} else {
return FormValidation.ok();
}
}
public FormValidation doCheckStashServerBaseUrl(
@QueryParameter String value)
throws IOException, ServletException {
// calculate effective url from global and local config
String url = value;
if ((url != null) && (!url.trim().equals(""))) {
url = url.trim();
} else {
url = stashRootUrl != null ? stashRootUrl.trim() : null;
}
if ((url == null) || url.equals("")) {
return FormValidation.error(
"Please specify a valid URL here or in the global "
+ "configuration");
} else {
try {
new URL(url);
return FormValidation.ok();
} catch (Exception e) {
return FormValidation.error(
"Please specify a valid URL here or in the global "
+ "configuration!");
}
}
}
@SuppressWarnings("rawtypes")
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
public String getDisplayName() {
return "Notify Stash Instance";
}
@Override
public boolean configure(
StaplerRequest req,
JSONObject formData) throws FormException {
// to persist global configuration information,
// set that to properties and call save().
stashRootUrl = formData.getString("stashRootUrl");
ignoreUnverifiedSsl = formData.getBoolean("ignoreUnverifiedSsl");
includeBuildNumberInKey = formData.getBoolean("includeBuildNumberInKey");
if (formData.has("credentialsId") && StringUtils.isNotBlank(formData.getString("credentialsId"))) {
credentialsId = formData.getString("credentialsId");
}
if (formData.has("projectKey")) {
projectKey = formData.getString("projectKey");
}
prependParentProjectKey = formData.getBoolean("prependParentProjectKey");
disableInprogressNotification = formData.getBoolean("disableInprogressNotification");
save();
return super.configure(req,formData);
}
}
// non-public members ------------------------------------------------------
/**
* Notifies the configured Stash server by POSTing the build results
* to the Stash build API.
*
* @param logger the logger to use
* @param build the build to notify Stash of
* @param commitSha1 the SHA1 of the built commit
* @param listener the build listener for logging
* @param state the state of the build as defined by the Stash API.
*/
private NotificationResult notifyStash(
final PrintStream logger,
final AbstractBuild<?, ?> build,
final String commitSha1,
final BuildListener listener,
final StashBuildState state) throws Exception {
HttpEntity stashBuildNotificationEntity
= newStashBuildNotificationEntity(build, state, listener);
HttpPost req = createRequest(stashBuildNotificationEntity, build.getProject(), commitSha1);
HttpClient client = getHttpClient(logger,build);
try {
HttpResponse res = client.execute(req);
if (res.getStatusLine().getStatusCode() != 204) {
return NotificationResult.newFailure(
EntityUtils.toString(res.getEntity()));
} else {
return NotificationResult.newSuccess();
}
} finally {
client.getConnectionManager().shutdown();
}
}
/**
* A helper method to obtain the configured credentials.
*
* @param clazz The type of {@link com.cloudbees.plugins.credentials.Credentials} to return.
* @param project The hierarchical project context within which the credentials are searched for.
* @return The first credentials of the given type that are found withing the project hierarchy, or null otherwise.
*/
private <T extends Credentials> T getCredentials(final Class<T> clazz, final Item project) {
DescriptorImpl descriptor = getDescriptor();
String credentialsId = getCredentialsId();
if (StringUtils.isBlank(credentialsId) && descriptor != null) {
credentialsId = descriptor.getCredentialsId();
}
if (StringUtils.isNotBlank(credentialsId) && clazz != null && project != null) {
return CredentialsMatchers.firstOrNull(
lookupCredentials(clazz, project, ACL.SYSTEM, new ArrayList<DomainRequirement>()),
CredentialsMatchers.withId(credentialsId));
}
return null;
}
/**
* Returns all credentials which are available to the specified {@link Authentication}
* for use by the specified {@link Item}.
*
* @param type the type of credentials to get.
* @param authentication the authentication.
* @param item the item.
* @param domainRequirements the credential domains to match.
* @param <C> the credentials type.
* @return the list of credentials.
*/
protected <C extends Credentials> List<C> lookupCredentials(Class<C> type, Item item, Authentication authentication, ArrayList<DomainRequirement> domainRequirements) {
return CredentialsProvider.lookupCredentials(type, item, authentication, domainRequirements);
}
/**
* Returns the HTTP POST request ready to be sent to the Stash build API for
* the given build and change set.
*
* @param stashBuildNotificationEntity a entity containing the parameters
* for Stash
* @param commitSha1 the SHA1 of the commit that was built
* @return the HTTP POST request to the Stash build API
*/
private HttpPost createRequest(
final HttpEntity stashBuildNotificationEntity,
final Item project,
final String commitSha1) {
String url = stashServerBaseUrl;
DescriptorImpl descriptor = getDescriptor();
if ("".equals(url) || url == null)
url = descriptor.getStashRootUrl();
HttpPost req = new HttpPost(
url
+ "/rest/build-status/1.0/commits/"
+ commitSha1);
// If we have a credential defined then we need to determine if it
// is a basic auth
UsernamePasswordCredentials usernamePasswordCredentials =
getCredentials(UsernamePasswordCredentials.class, project);
if (usernamePasswordCredentials != null) {
req.addHeader(BasicScheme.authenticate(
new org.apache.http.auth.UsernamePasswordCredentials(
usernamePasswordCredentials.getUsername(),
usernamePasswordCredentials.getPassword().getPlainText()),
"UTF-8",
false));
}
req.addHeader("Content-type", "application/json");
req.setEntity(stashBuildNotificationEntity);
return req;
}
/**
* Returns the HTTP POST entity body with the JSON representation of the
* builds result to be sent to the Stash build API.
*
* @param build the build to notify Stash of
* @return HTTP entity body for POST to Stash build API
*/
private HttpEntity newStashBuildNotificationEntity(
final AbstractBuild<?, ?> build,
final StashBuildState state,
BuildListener listener) throws UnsupportedEncodingException {
JSONObject json = new JSONObject();
json.put("state", state.name());
json.put("key", abbreviate(getBuildKey(build, listener), MAX_FIELD_LENGTH));
// This is to replace the odd character Jenkins injects to separate
// nested jobs, especially when using the Cloudbees Folders plugin.
// These characters cause Stash to throw up.
String fullName = StringEscapeUtils.
escapeJavaScript(build.getFullDisplayName()).
replaceAll("\\\\u00BB", "\\/");
json.put("name", abbreviate(fullName, MAX_FIELD_LENGTH));
json.put("description", abbreviate(getBuildDescription(build, state), MAX_FIELD_LENGTH));
json.put("url", abbreviate(getRootUrl().concat(build.getUrl()), MAX_URL_FIELD_LENGTH));
return new StringEntity(json.toString(), "UTF-8");
}
private static String abbreviate(String text, int maxWidth) {
if (text == null) {
return null;
}
if (maxWidth < 4) {
throw new IllegalArgumentException("Minimum abbreviation width is 4");
}
if (text.length() <= maxWidth) {
return text;
}
return text.substring(0, maxWidth - 3) + "...";
}
/**
* Return the old-fashion build key
*
* @param build the build to notify Stash of
* @return default build key
*/
private String getDefaultBuildKey(final AbstractBuild<?, ?> build) {
StringBuilder key = new StringBuilder();
key.append(build.getProject().getName());
if (includeBuildNumberInKey
|| getDescriptor().isIncludeBuildNumberInKey()) {
key.append('-').append(build.getNumber());
}
key.append('-').append(getRootUrl());
return key.toString();
}
/**
* Returns the build key used in the Stash notification. Includes the
* build number depending on the user setting.
*
* @param build the build to notify Stash of
* @return the build key for the Stash notification
*/
private String getBuildKey(final AbstractBuild<?, ?> build,
BuildListener listener) {
StringBuilder key = new StringBuilder();
if (prependParentProjectKey || getDescriptor().isPrependParentProjectKey()){
if (null != build.getParent().getParent()) {
key.append(build.getParent().getParent().getFullName()).append('-');
}
}
String overriddenKey = (projectKey != null && projectKey.trim().length() > 0) ? projectKey : getDescriptor().getProjectKey();
if (overriddenKey != null && overriddenKey.trim().length() > 0) {
PrintStream logger = listener.getLogger();
try {
key.append(TokenMacro.expandAll(build, listener, projectKey));
} catch (IOException e) {
logger.println("Cannot expand build key from parameter. Processing with default build key");
e.printStackTrace(logger);
key.append(getDefaultBuildKey(build));
} catch (InterruptedException e) {
logger.println("Cannot expand build key from parameter. Processing with default build key");
e.printStackTrace(logger);
key.append(getDefaultBuildKey(build));
} catch (MacroEvaluationException e) {
logger.println("Cannot expand build key from parameter. Processing with default build key");
e.printStackTrace(logger);
key.append(getDefaultBuildKey(build));
}
} else {
key.append(getDefaultBuildKey(build));
}
return StringEscapeUtils.escapeJavaScript(key.toString());
}
/**
* Returns the description of the build used for the Stash notification.
* Uses the build description provided by the Jenkins job, if available.
*
* @param build the build to be described
* @param state the state of the build
* @return the description of the build
*/
private String getBuildDescription(
final AbstractBuild<?, ?> build,
final StashBuildState state) {
if (build.getDescription() != null
&& build.getDescription().trim().length() > 0) {
return build.getDescription();
} else {
switch (state) {
case INPROGRESS:
return "building on Jenkins @ "
+ getRootUrl();
default:
return "built by Jenkins @ "
+ getRootUrl();
}
}
}
}